﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WcfContrib.Client.Behaviors;
using WcfContrib.Resources;
using WcfContrib.Client;
using System.Reflection;
using System.ServiceModel;

namespace WcfContrib.Extensions.Caching
{
    class OutputCacheBehavior : IClientOperationInspectorBehavior, IClientOperationInvokerBehavior
    {
        class KeyDescription
        {
            public OutputCacheItemKey Key { get; private set; }
            public bool IsProvidedFromCache { get; set; }
            public object AsyncState { get; private set; }

            public KeyDescription(OutputCacheItemKey key, object asyncState)
            {
                Key = key;
                AsyncState = asyncState;
            }
        }

        OutputCacheInterval cacheInterval;

        string Action { get; set; }
        bool CompareEnumerableItems { get; set; }
        bool IgnoreDefaultValue { get; set; }
        bool AsyncPattern { get; set; }
        Dictionary<int, IEqualityComparer<object>> Comparers { get; set; }
        object defaultValue;

        public OutputCacheBehavior(string action, bool asyncPattern, OutputCachePolicy policy, TimeSpan interval, bool compareEnumerableItems, bool ignoreDefaultValue, object defaultValue, Dictionary<int, IEqualityComparer<object>> comparers)
        {
            if (interval == null || interval.TotalMilliseconds < 0 && policy != OutputCachePolicy.Indefinite)
                throw new ArgumentException("cacheTime", ExceptionMessages.OutputCache_NoCacheTime);

            if (asyncPattern)
            {
                action = action.Remove(0, ClientChannel.MethodBeginInitial.Length);
            }

            this.IgnoreDefaultValue = ignoreDefaultValue;
            this.AsyncPattern = asyncPattern;
            this.Action = action;
            this.CompareEnumerableItems = compareEnumerableItems;
            this.defaultValue = defaultValue;

            cacheInterval = new OutputCacheInterval
            {
                Interval = interval,
                Policy = policy
            };

            this.Comparers = comparers;
        }

        #region IClientOperationInspectorBehavior Members

        public object BeforeCall(ClientChannel clientChannel, ChannelInvokeContext context)
        {
            object asyncState = null;
            object[] parameters = context.Parameters;
            if (AsyncPattern && parameters != null)
            {
                if (context.Method.Name.StartsWith(ClientChannel.MethodEndInitial))
                {
                    parameters = context.Extensions.Find<BeginParameterStateExtension>().Parameters;
                }
                else 
                {
                    //begin method
                    if (parameters.Length >= 2)
                    {
                        asyncState = parameters[parameters.Length - 1];
                        parameters = parameters.Take(parameters.Length - 2).ToArray();
                    }

                    context.Extensions.Add(new BeginParameterStateExtension(parameters));
                }
            }

            return new KeyDescription(new OutputCacheItemKey(Action, CompareEnumerableItems, parameters, Comparers), asyncState);
        }

        public void AfterCall(ClientChannel clientChannel, ChannelInvokeContext context, object result, object correlationState)
        {
            KeyDescription key = correlationState as KeyDescription;

            if (!key.IsProvidedFromCache && 
                (!IgnoreDefaultValue || !Object.Equals(defaultValue, result))
                && (!AsyncPattern || context.Method.Name.StartsWith(ClientChannel.MethodEndInitial)))
            {
                OutputCacheController.Current.Provider.AddItem(key.Key, cacheInterval.CloneCore(), result);
            }
        }

        #endregion

        #region IClientOperationInvokerBehavior Members

        public object ProvideResult(ClientChannel clientChannel, ChannelInvokeContext context, object correlationState, out bool providedResult)
        {
            KeyDescription key = correlationState as KeyDescription;

            object result;
            providedResult = OutputCacheController.Current.Provider.TryGetItem(key.Key, out result);
            key.IsProvidedFromCache = providedResult;

            if (providedResult && AsyncPattern && context.Method.Name.StartsWith(ClientChannel.MethodBeginInitial))
            {
                result = new ChannelInvokeAsyncResult(result, key.AsyncState);
            }
            
            return result;
        }

        #endregion
    }

    class BeginParameterStateExtension : IExtension<ChannelInvokeContext>
    {
        public object[] Parameters { get; private set; }
        
        public BeginParameterStateExtension(object[] parameters)
        {
            Parameters = parameters;
        }

        #region IExtension<ChannelInvokeContext> Members

        public void Attach(ChannelInvokeContext owner)
        {
        }

        public void Detach(ChannelInvokeContext owner)
        {
        }

        #endregion
    }
}
